package ly.count.ohos.sdk;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ModuleRemoteConfig extends ModuleBase {
    boolean isUpdateRemoteConfigAfterIdChange = false;

    RemoteConfig remoteConfigInterface = null;

    ModuleLog moduleLog;

    ModuleRemoteConfig(Countly cly, CountlyConfig config) {
        super(cly);

        moduleLog = cly.L;

        moduleLog.v("[ModuleRemoteConfig] Initialising");

        mCly.setRemoteConfigAutomaticDownload(config.enableRemoteConfigAutomaticDownload, config.remoteConfigCallback);

        remoteConfigInterface = new RemoteConfig();
    }

    /**
     * Internal call for updating remote config keys
     *
     * @param keysOnly set if these are the only keys to update
     * @param keysExcept set if these keys should be ignored from the update
     * @param connectionQueue 数组队列
     * @param isRequestShouldBeDelayed this is set to true in case of update after a deviceId change
     * @param callback called after the update is done
     */
    void updateRemoteConfigValues(final String[] keysOnly,
                                  final String[] keysExcept,
                                  final ConnectionQueue connectionQueue,
                                  final boolean isRequestShouldBeDelayed,
                                  final RemoteConfigCallback callback) {
        moduleLog.d("[ModuleRemoteConfig] Updating remote config values, requestShouldBeDelayed:["
            + isRequestShouldBeDelayed + "]");
        String keysInclude = null;
        String keysExclude = null;

        if (keysOnly != null && keysOnly.length > 0) {
            // include list takes precedence
            // if there is at least one item, use it
            JSONArray includeArray = new JSONArray();
            for (String key : keysOnly) {
                includeArray.put(key);
            }
            keysInclude = includeArray.toString();
        } else if (keysExcept != null && keysExcept.length > 0) {
            // include list was not used, use the exclude list
            JSONArray excludeArray = new JSONArray();
            for (String key : keysExcept) {
                excludeArray.put(key);
            }
            keysExclude = excludeArray.toString();
        }

        if (connectionQueue.getDeviceId().getId() == null) {
            // device ID is null, abort
            moduleLog.d("[ModuleRemoteConfig] RemoteConfig value update was aborted, deviceID is null");

            if (callback != null) {
                callback.callback("Can't complete call, device ID is null");
            }

            return;
        }

        if (connectionQueue.getDeviceId().temporaryIdModeEnabled() || connectionQueue.queueContainsTemporaryIdItems()) {
            // temporary id mode enabled, abort
            moduleLog.d("[ModuleRemoteConfig] RemoteConfig value update was aborted, temporary device ID mode is set");

            if (callback != null) {
                callback.callback("Can't complete call, temporary device ID is set");
            }

            return;
        }

        String requestData = connectionQueue.prepareRemoteConfigRequest(keysInclude, keysExclude);
        moduleLog.d("[ModuleRemoteConfig] RemoteConfig requestData:[" + requestData + "]");

        ConnectionProcessor cp = connectionQueue.createConnectionProcessor();

        (new ImmediateRequestMaker()).execute(requestData, "/o/sdk", cp,
            isRequestShouldBeDelayed, new ImmediateRequestMaker.InternalFeedbackRatingCallback() {
                @Override
                public void callback(JSONObject checkResponse) {
                    moduleLog.d("[ModuleRemoteConfig] Processing remote config received response, "
                        + "received response is null:[" + (checkResponse == null) + "]");
                    if (checkResponse == null) {
                        if (callback != null) {
                            callback.callback("Encountered problem while trying to reach the server, "
                                + "possibly no internet connection");
                        }
                        return;
                    }

                    // merge the new values into the current ones
                    RemoteConfigValueStore rcvs = loadConfig();
                    if (keysExcept == null && keysOnly == null) {
                        // in case of full updates, clear old values
                        rcvs.values = new JSONObject();
                    }
                    rcvs.mergeValues(checkResponse);

                    Countly.sharedInstance().L.d("[ModuleRemoteConfig] Finished remote config processing, "
                        + "starting saving");

                    saveConfig(rcvs);

                    Countly.sharedInstance().L.d("[ModuleRemoteConfig] Finished remote config saving");

                    if (callback != null) {
                        callback.callback(null);
                    }
                }
            }, mCly.L);
    }

    Object getValue(String key) {
        RemoteConfigValueStore rcvs = loadConfig();
        return rcvs.getValue(key);
    }

    void saveConfig(RemoteConfigValueStore rcvs) {
        mCly.connectionQueue_.getCountlyStore().setRemoteConfigValues(rcvs.dataToString());
    }

    RemoteConfigValueStore loadConfig() {
        CountlyStore cs = mCly.connectionQueue_.getCountlyStore();
        String rcvsString = cs.getRemoteConfigValues();

        // noinspection UnnecessaryLocalVariable
        RemoteConfigValueStore rcvs = RemoteConfigValueStore.dataFromString(rcvsString);
        return rcvs;
    }

    void clearValueStore() {
        mCly.connectionQueue_.getCountlyStore().setRemoteConfigValues("");
    }

    Map<String, Object> getAllRemoteConfigValuesInternal() {
        RemoteConfigValueStore rcvs = loadConfig();
        return rcvs.getAllValues();
    }

    static class RemoteConfigValueStore {
        public JSONObject values = new JSONObject();

        // add new values to the current storage
        public void mergeValues(JSONObject newValues) {
            if (newValues == null) {
                return;
            }

            Iterator<String> iter = newValues.keys();
            while (iter.hasNext()) {
                String key = iter.next();
                try {
                    Object value = newValues.get(key);
                    values.put(key, value);
                } catch (Exception e) {
                    Countly.sharedInstance().L.e("[RemoteConfigValueStore] Failed merging new remote config values");
                }
            }
        }

        private RemoteConfigValueStore(JSONObject values) {
            this.values = values;
        }

        public Object getValue(String key) {
            return values.opt(key);
        }

        public Map<String, Object> getAllValues() {
            Map<String, Object> ret = new HashMap<>();

            Iterator<String> keys = values.keys();

            while (keys.hasNext()) {
                String key = keys.next();

                try {
                    ret.put(key, values.get(key));
                } catch (Exception ex) {
                    Countly.sharedInstance().L.e("[RemoteConfigValueStore] "
                        + "Got JSON exception while calling 'getAllValues': " + ex.toString());
                }
            }

            return ret;
        }

        public static RemoteConfigValueStore dataFromString(String storageString) {
            if (storageString == null || storageString.isEmpty()) {
                return new RemoteConfigValueStore(new JSONObject());
            }

            JSONObject values;
            try {
                values = new JSONObject(storageString);
            } catch (JSONException e) {
                Countly.sharedInstance().L.e("[RemoteConfigValueStore] "
                    + "Couldn't decode RemoteConfigValueStore successfully: " + e.toString());
                values = new JSONObject();
            }
            return new RemoteConfigValueStore(values);
        }

        public String dataToString() {
            return values.toString();
        }
    }

    void clearAndDownloadAfterIdChange() {
        moduleLog.v("[RemoteConfig] Clearing remote config values and preparing to download after ID update");

        mCly.remoteConfig().clearStoredValues();
        if (mCly.remoteConfigAutomaticUpdateEnabled && mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
            isUpdateRemoteConfigAfterIdChange = true;
        }
    }

    @Override
    void deviceIdChanged() {
        moduleLog.v("[RemoteConfig] Device ID changed will update values: [" + isUpdateRemoteConfigAfterIdChange + "]");

        if (isUpdateRemoteConfigAfterIdChange) {
            isUpdateRemoteConfigAfterIdChange = false;
            updateRemoteConfigValues(null, null, mCly.connectionQueue_, true, null);
        }
    }

    @Override
    public void initFinished(CountlyConfig config) {
        // update remote config_ values if automatic update is enabled and we are not in temporary id mode
        if (mCly.remoteConfigAutomaticUpdateEnabled
            && mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)
            && !mCly.connectionQueue_.getDeviceId().temporaryIdModeEnabled()) {
            moduleLog.d("[Init] Automatically updating remote config values");
            updateRemoteConfigValues(null, null, mCly.connectionQueue_, false, mCly.remoteConfigInitCallback);
        }
    }

    @Override
    public void halt() {
        remoteConfigInterface = null;
    }

    public class RemoteConfig {
        /**
         * Clear all stored remote config_ values
         */
        public void clearStoredValues() {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Calling 'clearStoredValues'");

                clearValueStore();
            }
        }

        public Map<String, Object> getAllValues() {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Calling 'getAllValues'");

                if (!mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
                    return null;
                }

                return getAllRemoteConfigValuesInternal();
            }
        }

        /**
         * Get the stored value for the provided remote config_ key
         *
         * @param key
         * @return Object
         */
        public Object getValueForKey(String key) {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Calling remoteConfigValueForKey, " + key);

                if (!mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
                    return null;
                }

                return getValue(key);
            }
        }

        /**
         * Manual remote config update call. Will update all keys except the ones provided
         *
         * @param keysToExclude
         * @param callback
         */
        public void updateExceptKeys(String[] keysToExclude, RemoteConfigCallback callback) {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Manually calling to updateRemoteConfig with exclude keys");

                if (!mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
                    if (callback != null) {
                        callback.callback("No consent given");
                    }
                    return;
                }
                if (keysToExclude == null) {
                    moduleLog.w("[RemoteConfig] updateRemoteConfigExceptKeys passed 'keys to ignore' array is null");
                }
                updateRemoteConfigValues(null, keysToExclude, mCly.connectionQueue_, false, callback);
            }
        }

        /**
         * Manual remote config_ update call. Will only update the keys provided.
         *
         * @param keysToInclude
         * @param callback
         */
        public void updateForKeysOnly(String[] keysToInclude, RemoteConfigCallback callback) {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Manually calling to updateRemoteConfig with include keys");
                if (!mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
                    if (callback != null) {
                        callback.callback("No consent given");
                    }
                    return;
                }
                if (keysToInclude == null) {
                    moduleLog.w("[RemoteConfig] updateRemoteConfigExceptKeys passed 'keys to include' array is null");
                }
                updateRemoteConfigValues(keysToInclude, null, mCly.connectionQueue_, false, callback);
            }
        }

        /**
         * Manually update remote config_ values
         *
         * @param callback
         */
        public void update(RemoteConfigCallback callback) {
            synchronized (mCly) {
                moduleLog.i("[RemoteConfig] Manually calling to updateRemoteConfig");

                if (!mCly.getConsent(Countly.CountlyFeatureNames.remoteConfig)) {
                    return;
                }

                updateRemoteConfigValues(null, null, mCly.connectionQueue_, false, callback);
            }
        }
    }
}
